﻿/*
 * Copyright 2007 (c) Tim Knip, ascollada.org.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
 
package org.papervision3d.objects 
{
	import flash.events.*;
	import flash.geom.Matrix;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	import org.papervision3d.cameras.Camera3D;
	import org.papervision3d.cameras.FreeCamera3D;
	
	import org.ascollada.ASCollada;
	import org.ascollada.core.*;
	import org.ascollada.fx.*;
	import org.ascollada.io.*;
	import org.ascollada.types.*;
	import org.ascollada.utils.Logger;
	
	import org.papervision3d.animation.*;
	import org.papervision3d.animation.curves.*;
	import org.papervision3d.core.*;
	import org.papervision3d.core.geom.*;
	import org.papervision3d.core.geom.renderables.*;
	import org.papervision3d.core.proto.*;
	import org.papervision3d.events.*;
	import org.papervision3d.materials.*;
	import org.papervision3d.objects.DisplayObject3D;
	
	/**
	 * 
	 */
	public class DAE extends DisplayObject3D
	{
		/** the filename */
		public var filename:String;
		
		/** */
		public var fileTitle:String;
		
		/** */
		public var baseUrl:String;
		
		/** */
		public var controller:AnimationController;
		
		/** */
		public var rootNode:DisplayObject3D;
		
		/** */
		public var cameras:Array;
		
		/** */
		public function get document():DaeDocument { return _document; }
		
		/**
		 * constructor.
		 * 
		 * @param	asset		filename or ByteArray of the dae.
		 * @param	materials	optional MaterialsList, will override any material defined in the dae.
		 * @param	async		load some stuff async to prevent timeout errors.
		 * @return
		 */
		public function DAE(asset:*, materials:MaterialsList = null, async:Boolean = false):void
		{
			_reader = new DaeReader( async );
			
			this.materials = materials || new MaterialsList();
			
			this.filename = asset is String ? String(asset) : "../../../meshes/rawdata_dae";
			
			this.materials.addMaterial(new WireframeMaterial(0xffff00), "line");
			
			// make sure we've got forward slashes!
			this.filename = this.filename.split("\\").join("/");
				
			if( this.filename.indexOf("/") != -1 )
			{
				// dae is located in a sub-directory of the swf.
				var parts:Array = this.filename.split("/");
				this.fileTitle = String( parts.pop() );
				this.baseUrl = parts.join("/");
			}
			else
			{
				// dae is located in root directory of swf.
				this.fileTitle = this.filename;
				this.baseUrl = "";
			}
				
			super( fileTitle, new GeometryObject3D() );
				
			_scaling = 1;
			
			if( asset is ByteArray || asset is XML )
			{
				_reader.addEventListener( Event.COMPLETE, buildDAE );
				_reader.addEventListener( ProgressEvent.PROGRESS, loadProgressHandler );
				
				_asset = asset;
				
				var timer:Timer = new Timer(500);
				timer.addEventListener(TimerEvent.TIMER, delayedLoadHandler);
				timer.start();
			}
			else if( asset is String )
			{		
				_reader.addEventListener( Event.COMPLETE, buildDAE );
				_reader.addEventListener( ProgressEvent.PROGRESS, loadProgressHandler );
				_reader.read( this.filename );
			}
		}
		
		/**
		 * 
		 * @param	name
		 * @return
		 */
		override public function getChildByName(name:String):DisplayObject3D
		{
			if( !name ) return null
			return this.findChildByName(this, name);
		}
		
		/**
		 * 
		 * @param	parent
		 * @param	camera
		 * @param	sorted
		 * @return
		 */
		override public function project( parent:DisplayObject3D, camera:CameraObject3D, sorted :Array=null ):Number
		{
			return super.project(parent, camera, sorted);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function buildDAE( event:Event ):void
		{
			if(_reader.hasEventListener(Event.COMPLETE))
				_reader.removeEventListener( Event.COMPLETE, buildDAE );
				
			this.controller = new AnimationController();
			this.cameras = new Array();
			
			_document = _reader.document;
			_queuedBitmaps = new Object();
			_skins = new Dictionary();
			_materialTextureSets = new Dictionary();
			
			if( _document.numQueuedGeometries ) 
			{
				_reader.addEventListener( Event.COMPLETE, geometryCompleteHandler );
				_reader.addEventListener( ProgressEvent.PROGRESS, geometryProgressHandler );
			
				_reader.readGeometries();
			}
			else
			{
				geometryCompleteHandler(null);
			}
		}
				
		/**
		 * 
		 * @param	node
		 * @param	instance
		 * @return
		 */
		private function buildControllers( node:DaeNode, instance:DisplayObject3D ):void
		{
		}
		
		/**
		 * 
		 * @param	primitive
		 * @param	geometry
		 * @return
		 */
		private function buildFaces( primitive:DaePrimitive, geometry:GeometryObject3D, instance:DisplayObject3D ):void {
			
			var i:int, j:int, k:int;
			
			var material:MaterialObject3D = this.materials.getMaterialByName(primitive.material);
			
			material = material || MaterialObject3D.DEFAULT;
			
			var texcoords:Array = new Array();
			
			// retreive correct texcoord-set for the material.
			var obj:DaeBindVertexInput = _materialTextureSets[primitive.material];
			var setID:int = (obj is DaeBindVertexInput) ? obj.input_set : 0;
			var texCoordSet:Array = primitive.getTexCoords(setID);
			
			// texture coords
			for( i = 0; i < texCoordSet.length; i++ ) 
			{
				var t:Array = texCoordSet[i];
				texcoords.push( new NumberUV( t[0], t[1] ) );
			}
			
			var hasUV:Boolean = (texcoords.length == primitive.vertices.length);

			var idx:Array = new Array();
			var v:Array = new Array();
			var uv:Array = new Array();
			
			switch( primitive.type ) 
			{
				// Each line described by the mesh has two vertices. The first line is formed 
				// from first and second vertices. The second line is formed from the third and fourth 
				// vertices and so on.
				case ASCollada.DAE_LINES_ELEMENT:
					for( i = 0; i < primitive.vertices.length; i += 2 ) 
					{
						v[0] = geometry.vertices[ primitive.vertices[i] ];
						v[1] = geometry.vertices[ primitive.vertices[i+1] ];
						uv[0] = hasUV ? texcoords[  i  ] : new NumberUV();
						uv[1] = hasUV ? texcoords[ i+1 ] : new NumberUV();
						//geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[1]], material, [uv[0], uv[1], uv[1]]) );
					}
					break;
					
				// Each line-strip described by the mesh has an arbitrary number of vertices. Each line 
				// segment within the line-strip is formed from the current vertex and the preceding 
				// vertex.
				case ASCollada.DAE_LINESTRIPS_ELEMENT:
					for( i = 1; i < primitive.vertices.length; i++ ) 
					{
						v[0] = geometry.vertices[ primitive.vertices[i-1] ];
						v[1] = geometry.vertices[ primitive.vertices[i] ];
						uv[0] = hasUV ? texcoords[i-1] : new NumberUV();
						uv[1] = hasUV ? texcoords[i] : new NumberUV();
						//geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[1]], material, [uv[0], uv[1], uv[1]]) );
					}
					break;
					
				// simple triangles
				case ASCollada.DAE_TRIANGLES_ELEMENT:
					for( i = 0, j = 0; i < primitive.vertices.length; i += 3, j++ ) 
					{
						idx[0] = primitive.vertices[i];
						idx[1] = primitive.vertices[i+1];
						idx[2] = primitive.vertices[i+2];
						
						v[0] = geometry.vertices[ idx[0] ];
						v[1] = geometry.vertices[ idx[1] ];
						v[2] = geometry.vertices[ idx[2] ];
						
						uv[0] = hasUV ? texcoords[  i  ] : new NumberUV();
						uv[1] = hasUV ? texcoords[ i+1 ] : new NumberUV();
						uv[2] = hasUV ? texcoords[ i+2 ] : new NumberUV();
									
						geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[2]], material, [uv[0], uv[1], uv[2]]) );
					}
					break;
				
				// Each triangle described by the mesh has three vertices. 
				// The first triangle is formed from the first, second, and third vertices. 
				// Each subsequent triangle is formed from the current vertex, reusing the 
				// first and the previous vertices.
				case ASCollada.DAE_TRIFANS_ELEMENT:
					v[0] = geometry.vertices[ primitive.vertices[0] ];
					v[1] = geometry.vertices[ primitive.vertices[1] ];
					v[2] = geometry.vertices[ primitive.vertices[2] ];
					uv[0] = hasUV ? texcoords[0] : new NumberUV();
					uv[1] = hasUV ? texcoords[1] : new NumberUV();
					uv[2] = hasUV ? texcoords[2] : new NumberUV();
					
					geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[2]], material, [uv[0], uv[1], uv[2]]) );
					
					for( i = 3; i < primitive.vertices.length; i++ ) 
					{
						v[1] = geometry.vertices[ primitive.vertices[i-1] ];
						v[2] = geometry.vertices[ primitive.vertices[i] ];
						uv[1] = hasUV ? texcoords[i-1] : new NumberUV();
						uv[2] = hasUV ? texcoords[i] : new NumberUV();
						
						geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[2]], material, [uv[0], uv[1], uv[2]]) );
					}
					break;
				
				// Each triangle described by the mesh has three vertices. The first triangle 
				// is formed from the first, second, and third vertices. Each subsequent triangle 
				// is formed from the current vertex, reusing the previous two vertices.
				case ASCollada.DAE_TRISTRIPS_ELEMENT:
					v[0] = geometry.vertices[ primitive.vertices[0] ];
					v[1] = geometry.vertices[ primitive.vertices[1] ];
					v[2] = geometry.vertices[ primitive.vertices[2] ];
					uv[0] = hasUV ? texcoords[0] : new NumberUV();
					uv[1] = hasUV ? texcoords[1] : new NumberUV();
					uv[2] = hasUV ? texcoords[2] : new NumberUV();
					
					geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[2]], material, [uv[0], uv[1], uv[2]]) );
					
					for( i = 3; i < primitive.vertices.length; i++ ) 
					{
						v[0] = geometry.vertices[ primitive.vertices[i-2] ];
						v[1] = geometry.vertices[ primitive.vertices[i-1] ];
						v[2] = geometry.vertices[ primitive.vertices[i] ];
						uv[0] = hasUV ? texcoords[i-2] : new NumberUV();
						uv[1] = hasUV ? texcoords[i-1] : new NumberUV();
						uv[2] = hasUV ? texcoords[i] : new NumberUV();
						
						geometry.faces.push( new Triangle3D(instance, [v[0], v[1], v[2]], material, [uv[0], uv[1], uv[2]]) );
					}
					break;
					
				// polygon with *no* holes
				case ASCollada.DAE_POLYLIST_ELEMENT:
					for( i = 0, k = 0; i < primitive.vcount.length; i++ ) 
					{
						var poly:Array = new Array();
						var uvs:Array = new Array();
						for( j = 0; j < primitive.vcount[i]; j++ ) 
						{
							uvs.push( (hasUV ? texcoords[ k ] : new NumberUV()) );
							poly.push( geometry.vertices[primitive.vertices[k++]] );
						}
						
						v[0] = poly[0];
						uv[0] = uvs[0];
						
						for( j = 1; j < poly.length - 1; j++ )
						{
							v[1] = poly[j];
							v[2] = poly[j+1];
							uv[1] = uvs[j];
							uv[2] = uvs[j+1];
							geometry.faces.push( new Triangle3D( instance, [v[0], v[1], v[2]], material, [uv[0], uv[1], uv[2]]) );
						}
					}
					break;
				
				// polygon with holes...
				case ASCollada.DAE_POLYGONS_ELEMENT:
					break;
					
				default:
					break;
			}
		}
		
		/**
		 * builds the papervision geometry for a collada <mesh>.
		 * 
		 * @param	geometry
		 * @param	scaling
		 * @param	yUP
		 * 
		 * @return
		 */
		private function buildGeometry( instance:DisplayObject3D, geometry:DaeMesh, scaling:Number = 1.0, yUP:Boolean = true ):GeometryObject3D
		{
			var geom:GeometryObject3D = new GeometryObject3D();
			
			geom.vertices = new Array();
			geom.faces = new Array();
			
			var v:Array = geometry.vertices;
			var i:int;
			
			// create vertices
			for( i = 0; i < v.length; i++ )
				geom.vertices.push( new Vertex3D(v[i][0] * scaling, v[i][1] * scaling, v[i][2] * scaling) );
			
			for( var j:int = 0; j < geometry.primitives.length; j++ )
				buildFaces( geometry.primitives[j], geom, instance );

			Logger.trace( "new GeometryObject3D: v:" + geom.vertices.length + " f:" + geom.faces.length );
			return geom;
		}
		
		/**
		 *
		 * @return
		 */
		private function buildImagePath( meshFolderPath:String, imgPath:String ):String
		{
			var baseParts:Array = meshFolderPath.split("/");
			var imgParts:Array = imgPath.split("/");
			
			while( baseParts[0] == "." )
				baseParts.shift();
				
			while( imgParts[0] == "." )
				imgParts.shift();
				
			while( imgParts[0] == ".." )
			{
				imgParts.shift();
				baseParts.pop();
			}
						
			var imgUrl:String = baseParts.length > 1 ? baseParts.join("/") : (baseParts.length?baseParts[0]:"");
						
			imgUrl = imgUrl != "" ? imgUrl + "/" + imgParts.join("/") : imgParts.join("/");
			
			return imgUrl;
		}
		
		/**
		 * 
		 * @param	instance_material
		 * @return
		 */
		private function buildMaterial( instance_material:DaeInstanceMaterial ):MaterialObject3D
		{
			var material:MaterialObject3D = this.materials.getMaterialByName(instance_material.symbol);
						
			// get material from library
			var dae_material:DaeMaterial = _document.materials[ instance_material.target ];
			var effect:DaeEffect = _document.effects[ dae_material.effect ];
			var lambert:DaeLambert = effect.color as DaeLambert;
			
			if(lambert && lambert.diffuse.texture)
			{
				var semantic:String = lambert.diffuse.texture.texcoord;			
				var obj:DaeBindVertexInput = instance_material.findBindVertexInput(semantic);						
				if( obj )
					_materialTextureSets[instance_material.symbol] = obj;
			}
			
			// already in library
			if( material )
			{
				//materialCompleteHandler( new FileLoadEvent(FileLoadEvent.LOAD_COMPLETE, material.name) );
				return material;
			}
			
			if( effect && effect.texture_url )
			{				
				var img:DaeImage = _document.images[effect.texture_url];
				if( img )
				{
					var path:String = buildImagePath(this.baseUrl, img.init_from);
					
					material = new BitmapFileMaterial( path );
					material.addEventListener( FileLoadEvent.LOAD_COMPLETE, materialCompleteHandler );
					material.addEventListener( FileLoadEvent.LOAD_ERROR, materialErrorHandler );
					
					_queuedBitmaps[path] = instance_material.symbol;
				}
			}
			else if( effect.color is DaeLambert )
			{
				lambert = effect.color as DaeLambert;
				var c:DaeColorOrTexture = lambert.diffuse;

				var r:uint = Math.round(c.color[0] * 255);
				var g:uint = Math.round(c.color[1] * 255);
				var b:uint = Math.round(c.color[2] * 255);
				
				var col:uint = (r<<16 | g<<8 | b);
				
				material = new ShadedColorMaterial( col );
			}
			
			materials.addMaterial( material, instance_material.symbol ); // C4

			return material;
		}
		
		/**
		 * builds a papervision Matrix3D from a node's matrices array. @see DaeNode#matrices
		 * 
		 * @param	node
		 * 
		 * @return
		 */
		private function buildMatrix( node:DaeNode ):Matrix3D {
			var matrix:Matrix3D = Matrix3D.IDENTITY;
			for( var i:int = 0; i < node.matrices.length; i++ ) {
				matrix = Matrix3D.multiply( matrix, new Matrix3D(node.matrices[i]) );
			}
			return matrix;
		}
		
		/**
		 * 
		 * @param	spline
		 * @return
		 */
		private function buildSpline( spline:DaeSpline ):DisplayObject3D
		{
			var lines:Lines3D = new Lines3D(new LineMaterial(0xffff00, 0.5));
					
			for( var i:int = 0; i < spline.vertices.length; i++ )
			{
				var v0:Array = spline.vertices[i];
				var v1:Array = spline.vertices[(i+1) % spline.vertices.length];
				lines.addNewLine(0, v0[0], v0[1], v0[2], v1[0], v1[1], v1[2]);
			}
			
			return lines;
		}
		
		/**
		 * 
		 * @param	node
		 * 
		 * @return
		 */
		private function buildMesh( node:DaeNode ):DisplayObject3D
		{
			var newNode:DisplayObject3D = new Node3D(node.id, node.sid);
			
			for( var i:int = 0; i < node.geometries.length; i++ )
			{
				var geom_instance:DaeInstanceGeometry = node.geometries[i];
				
				var geom:DaeGeometry = _document.geometries[geom_instance.url];
				
				for each( var m:DaeInstanceMaterial in geom_instance.materials )
					buildMaterial( m );
						
				if( geom.splines.length )
				{
					for( var j:int = 0; j < geom.splines.length; j++ )
					{
						newNode.addChild( buildSpline(geom.splines[j]) );
					}
				}
				else if( geom.spline )
				{
					newNode.addChild( buildSpline(geom.spline) );
				}
				else
				{
					try
					{
						var mesh:TriangleMesh3D = new TriangleMesh3D(new WireframeMaterial(), new Array(), new Array());
						mesh.materials = this.materials;
						mesh.geometry = buildGeometry(newNode, geom.mesh);
						
						if( geom_instance.materials.length )
						{
							var mat:DaeInstanceMaterial = geom_instance.materials[0];
							
							var material:MaterialObject3D = this.materials.getMaterialByName(mat.symbol);
							if( material )
							{
								for each( var triangle:Triangle3D in mesh.geometry.faces )
									triangle.material = material;
							}
						}
					}
					catch( e:Error )
					{
						Logger.trace( "[ERROR] buildGeometry " + e.toString() );
						return null;
					}
					
					mesh.geometry.ready = true;
					newNode.addChild(mesh);
				}				
			}
			
			return newNode;
		}
		
		/**
		 * 
		 * @param	node
		 * @param	parent
		 * 
		 * @return
		 */
		private function buildScene( node:DaeNode, parent:DisplayObjectContainer3D ):DisplayObject3D
		{	
			var newNode:DisplayObject3D;
			var skinController:DaeInstanceController = findSkinController(node);
			var document:DaeDocument = this.document;
			var i:int, j:int;
			
			if( skinController )
			{
				newNode = buildSkin(node, skinController);
			}
			else if( node.geometries.length )
			{
				newNode = buildMesh(node);
			}
			else
			{
				newNode = new Node3D(node.id, node.sid);
			}
			
			if( !newNode ) return null;
			
			// node instances!
			for( j = 0; j < node.instance_nodes.length; j++ )
			{
				var instance_node:DaeInstanceNode = node.instance_nodes[j];
				var iNode:DaeNode = _document.getDaeNodeById(instance_node.url);
				
				var iObj:DisplayObject3D = getChildByName(iNode.id);
				
				if( iObj )
					newNode.addChild(iObj);
				else if(iNode)
					buildScene(iNode, newNode);
			}
			
			// cameras?
			for( j = 0; j < node.instance_cameras.length; j++ )
			{
				var cameraID:String = node.instance_cameras[j];
				var dae_camera:DaeCamera = _document.cameras[cameraID];
				if( !dae_camera ) 
					continue;

				var camera:CameraObject3D = dae_camera.target ? new Camera3D() : new FreeCamera3D();
				camera.extra = dae_camera;
				
				camera.copyTransform(buildMatrix(node));
				this.cameras.push(camera);
				
				newNode.addChild(camera);
			}
			
			var instance:DisplayObject3D = parent.addChild(newNode);
			
			for( i = 0; i < node.nodes.length; i++ )
				buildScene(node.nodes[i], instance);
				
			var matrix:Matrix3D = buildMatrix(node);
						
			instance.copyTransform( matrix );

			return instance;
		}
				
		/**
		 * 
		 * @param	node
		 * @param	controller
		 * @return
		 */
		private function buildSkin( node:DaeNode, controller:DaeInstanceController ):DisplayObject3D
		{
			var skinCtrl:DaeController = _document.controllers[controller.url];
			
			if( !skinCtrl || !skinCtrl.skin )
				return new DisplayObject3D(node.id);
				
			var skin:DaeSkin = skinCtrl.skin;
			var skinnedMesh:Skin3D = new Skin3D(new WireframeMaterial(), new Array(), new Array(), node.id);
			
			var geom:DaeGeometry = _document.geometries[ skin.source ];
				
			if( !geom )
			{
				if( _document.controllers[skin.source] )
				{
					var morhpCtrl:DaeController = _document.controllers[skin.source];
					if( morhpCtrl.morph )
						geom = _document.geometries[ morhpCtrl.morph.source ];
				}				
				if( !geom )
					throw new Error("Can't find geometry: " + skin.source );
			}
			
			for each( var m:DaeInstanceMaterial in controller.materials )
				buildMaterial( m );
						
			skinnedMesh.materials = this.materials;

			skinnedMesh.geometry = buildGeometry(skinnedMesh, geom.mesh);
			skinnedMesh.geometry.ready = true;
			
			skinnedMesh.bindPose = new Matrix3D(skin.bind_shape_matrix);
			
			_skins[ skinnedMesh ] = skin;
			
			skinnedMesh.transformVertices(skinnedMesh.bindPose);
			
			return skinnedMesh;
		}
		
		/**
		 * 
		 * @param	id
		 * @return
		 */
		private function findAnimationChannelsByID( id:String ):Array
		{
			var channels:Array = new Array();
		
			for each( var animation:DaeAnimation in _document.animations )
			{
				for each( var channel:DaeChannel in animation.channels )
				{
					var target:String = channel.target.split("/").shift() as String;
					if( target == id )
						channels.push(channel);
				}
			}
			return channels;
		}
		
		/**
		 * 
		 * @param	node
		 * @param	name
		 * @return
		 */
		private function findChildByName(node:DisplayObject3D, name:String):DisplayObject3D
		{
			if( node.name == name )
				return node;
				
			for each(var child:DisplayObject3D in node.children ) 
			{
				var n:DisplayObject3D = findChildByName(child, name);
				if( n )
					return n;
			}
			
			return null;
		}
		
		/**
		 * 
		 * @param	node
		 * @param	name
		 * @return
		 */
		private function findChildBySID(node:DisplayObject3D, sid:String):DisplayObject3D
		{
			if( node is Node3D && Node3D(node).sid == sid )
				return node;
				
			for each(var child:DisplayObject3D in node.children ) 
			{
				var n:DisplayObject3D = findChildBySID(child, sid);
				if( n )
					return n;
			}
			
			return null;
		}
		
		/**
		 * 
		 * @param	node
		 * @return
		 */
		private function findSkinController( node:DaeNode ):DaeInstanceController
		{
			for each( var controller:DaeInstanceController in node.controllers )
			{
				var control:DaeController = _document.controllers[controller.url];
				if( control.skin )
					return controller;
			}
			return null;
		}
		
		/**
		 * 
		 * @param	node
		 * @return
		 */
		private function linkAnimations( node:DisplayObject3D ):void
		{
			if( node is Node3D )
			{
				var joint:Node3D = node as Node3D;

				var channels:Array = findAnimationChannelsByID(node.name);
				
				if( channels && channels.length )
				{					
					var c:AnimationChannel = new AnimationChannel(node);
					
					for each( var channel:DaeChannel in channels )
					{
						var keys:Array = channel.input;
						var values:Array = channel.output;
						var interpolations:Array = channel.interpolations;
						
						var targetObject:String = channel.target.split("/")[1];
									
						var curve:AbstractCurve;
						
						if( keys.length && keys.length == values.length )
						{
							switch( targetObject )
							{
								case "transform":
									var matrices:Array = getMatrixKeyValues(values);
									
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N11, keys, matrices[0]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N12, keys, matrices[1]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N13, keys, matrices[2]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N14, keys, matrices[3]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N21, keys, matrices[4]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N22, keys, matrices[5]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N23, keys, matrices[6]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N24, keys, matrices[7]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N31, keys, matrices[8]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N32, keys, matrices[9]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N33, keys, matrices[10]));
									c.addCurve(new MatrixCurve(node.transform, MatrixCurve.MATRIX_N34, keys, matrices[11]));
									break;
			
								case "RotX.ANGLE":
								case "rotateX.ANGLE":
									c.addCurve(new RotationCurve(node, RotationCurve.ROTATION_X, keys, values));
									break;
								
								case "RotY.ANGLE":
								case "rotateY.ANGLE":
									c.addCurve(new RotationCurve(node, RotationCurve.ROTATION_Y, keys, values));
									break;
									
								case "RotZ.ANGLE":
								case "rotateZ.ANGLE":
									c.addCurve(new RotationCurve(node, RotationCurve.ROTATION_Z, keys, values));
									break;
								
								case "translation":
								case "translate":
									var translations:Array = getTranslationKeyValues(values);
									c.addCurve(new TranslationCurve(node, TranslationCurve.TRANSLATION_X, keys, translations[0]));
									c.addCurve(new TranslationCurve(node, TranslationCurve.TRANSLATION_Y, keys, translations[1]));
									c.addCurve(new TranslationCurve(node, TranslationCurve.TRANSLATION_Z, keys, translations[2]));
									break;
									
								default:
									Logger.trace( "UNKNOWN target: " + targetObject );
									break;
							}
						}
					}
					
					this.controller.addChannel( c );
				}
			}
			for each( var child:DisplayObject3D in node.children )
				linkAnimations( child );
		}
		
		/**
		 * 
		 * @param	values
		 * @return
		 */
		private function getMatrixKeyValues( values:Array ):Array
		{
			var i:int, j:int;
			var keyValues:Array = new Array(values.length);
			
			for( i = 0; i < values[0].length; i++ )
				keyValues[i] = new Array();
				
			for( i = 0; i < values.length; i++ )
				for( j = 0; j < values[i].length; j++ )
					keyValues[j][i] = values[i][j];
			return keyValues;
		}
		
		/**
		 * 
		 * @param	values
		 * @return
		 */
		private function getTranslationKeyValues( values:Array ):Array
		{
			var i:int;
			var keyValues:Array = new Array(3);
			
			keyValues[0] = new Array();
			keyValues[1] = new Array();
			keyValues[2] = new Array();
			
			for( i = 0; i < values.length; i++ )
			{
				keyValues[0][i] = values[i][0];
				keyValues[1][i] = values[i][1];
				keyValues[2][i] = values[i][2];
			}
			
			return keyValues;
		}
		
		/**
		 * 
		 * @param	node
		 * @return
		 */
		private function linkSkeletons( node:DisplayObject3D ):void
		{
			if( node is Skin3D )
			{
				var skinned:Skin3D = node as Skin3D;
				
				skinned.joints = new Array();
				
				var skin:DaeSkin = _skins[ skinned ];
				
				if( skin )
				{
					for( var i:int = 0; i < skin.joints.length; i++ )
					{
						var jid:String = skin.joints[i];
						
						if( !jid || !jid.length ) continue;
						
						var joint:DisplayObject3D = findChildByName(this, jid);
						if( !joint )
							joint = findChildBySID(this, jid);
							
						var bind:Array = skin.findJointBindMatrix2(jid);

						var matrix:Matrix3D = new Matrix3D(bind);
							
						Node3D(joint).bindMatrix = matrix;
						Node3D(joint).blendVerts = skin.findJointVertexWeightsByIDOrSID(jid);

						skinned.joints.push(joint);
					}
				}
			}
			
			for each( var child:DisplayObject3D in node.children )
				linkSkeletons( child );
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function animationCompleteHandler( event:Event ):void
		{
			linkAnimations(this);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function animationProgressHandler( event:ProgressEvent ):void
		{
			dispatchEvent(event);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function geometryCompleteHandler( event:Event ):void
		{
			var i:int;
			
			this.rootNode = addChild(new DisplayObject3D());
			
			try
			{
			for( i = 0; i < _document.vscene.nodes.length; i++ )
			{
				var node:DaeNode = _document.vscene.nodes[i];
				var obj:DisplayObject3D = buildScene( node, this.rootNode );
			}
			}
			catch( ex:Error )
			{
				Logger.error( "[ERROR] while creating scenegraph." + ex.toString() );
			}
			
			this.rootNode.scaleZ = -1;
			//this.rootNode.rotationX = 90;
			
			try
			{
				linkSkeletons(this);
			}
			catch( e:Error )
			{
				Logger.trace( "[ERROR] while linking skeletons: " + e.toString() );
			}
			
			for( i = 0; i < this.cameras.length; i++ )
			{
				var camera:CameraObject3D = this.cameras[i] as CameraObject3D;
				var dae_camera:DaeCamera = camera.extra as DaeCamera;
				
				if( camera is Camera3D && dae_camera.target )
				{
					var target:DisplayObject3D = getChildByName(dae_camera.target);
					Camera3D(camera).target = target;
				}
			}
			
			// should have something to show now...
			dispatchEvent(new Event(Event.COMPLETE));
			
			// but... there may be animations left to parse...
			_reader.addEventListener( Event.COMPLETE, animationCompleteHandler );
			_reader.addEventListener( ProgressEvent.PROGRESS, animationProgressHandler );
			
			_reader.readAnimations();
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function geometryProgressHandler( event:ProgressEvent ):void
		{
			dispatchEvent(event);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function delayedLoadHandler( event:TimerEvent ):void
		{
			loadProgressHandler(new ProgressEvent(ProgressEvent.PROGRESS,false,false,1,1));
			
			var timer:Timer = Timer(event.target);
			timer.stop();
			timer.removeEventListener(TimerEvent.TIMER, delayedLoadHandler);
			
			_reader.loadDocument(_asset);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function loadProgressHandler( event:ProgressEvent ):void
		{
			dispatchEvent(event);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function materialCompleteHandler( event:FileLoadEvent ):void
		{
			dispatchEvent(event);
		}
		
		/**
		 * 
		 * @param	event
		 * @return
		 */
		private function materialErrorHandler( event:FileLoadEvent ):void
		{
			var materialName:String = _queuedBitmaps[ event.file ];
			
			resetMaterial( this, materialName );
			
			Logger.trace( "[ERROR] material failed to load! " + materialName + " " + event.file );
		}
		
		/**
		 * 
		 * @param	node
		 * @param	materialName
		 * @param	newMaterial
		 * @return
		 */
		private function resetMaterial( node:DisplayObject3D, materialName:String, newMaterial:MaterialObject3D = null ):void
		{
			newMaterial = newMaterial || new ShadedColorMaterial(0xff0000);
			
			node.materials = node.materials || new MaterialsList();
			
			var mat:MaterialObject3D = node.materials.getMaterialByName(materialName);
			if( mat )
				node.materials.removeMaterialByName(materialName);
			
			node.materials.addMaterial(newMaterial, materialName);
			
			if( node.geometry )
			{
				for each( var face:Triangle3D in node.geometry.faces )
					face.material = newMaterial;
			}
			
			for each(var child:DisplayObject3D in node.children )
				resetMaterial( child, materialName, newMaterial );
		}
		
		/**
		 * 
		 * @param	node
		 * @param	indent
		 * @return
		 */
		public function dumpHierarchy(node:DisplayObject3D = null, indent:String = ""):String
		{
			node = node || this;
			
			var geom:GeometryObject3D = node.geometry;
			var sg:String = geom ? " v:"+geom.vertices.length + " f:" + geom.faces.length : "";
			
			var s:String = indent + node.name + sg + "\n";
			for each(var child:DisplayObject3D in node.children )
			{
				
				s += dumpHierarchy(child, indent + " -> ");		
			}
			return s;
		}
		
		/**
		 * 
		 * @return
		 */
		public override function toString():String
		{
			return dumpHierarchy();
		}
				
		private var _document:DaeDocument;
		
		private var _reader:DaeReader;
		
		private var _scaling:Number = 1;
		
		private var _queuedBitmaps:Object;
				
		private var _asset:*;
		
		private var _materialTextureSets:Dictionary;
		
		// keep track of skins
		private var _skins:Dictionary;
	}
}
